package com.xfcy.blog.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
import com.vladsch.flexmark.util.data.MutableDataSet;
import com.xfcy.blog.common.R;
import com.xfcy.blog.common.constant.Constant;
import com.xfcy.blog.common.constant.RedisConstant;
import com.xfcy.blog.common.constant.SqlConstant;
import com.xfcy.blog.common.enums.PublishEnum;
import com.xfcy.blog.common.enums.ResultCode;
import com.xfcy.blog.common.enums.SearchEnum;
import com.xfcy.blog.common.enums.YesOrNoEnum;
import com.xfcy.blog.dto.ArticleDTO;
import com.xfcy.blog.dto.ArticleTopDTO;
import com.xfcy.blog.entity.Category;
import com.xfcy.blog.entity.SystemConfig;
import com.xfcy.blog.entity.Tags;
import com.xfcy.blog.exception.CustomerException;
import com.xfcy.blog.exception.XfcyException;
import com.xfcy.blog.mapper.ArticleMapper;
import com.xfcy.blog.entity.Article;
import com.xfcy.blog.mapper.CategoryMapper;
import com.xfcy.blog.mapper.TagsMapper;
import com.xfcy.blog.service.ArticleService;
import com.xfcy.blog.service.ElasticSearchService;
import com.xfcy.blog.service.SystemConfigService;
import com.xfcy.blog.strategy.context.SearchStrategyContext;
import com.xfcy.blog.utils.BeanCopyUtil;
import com.xfcy.blog.vo.*;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;

/**
 * 博客文章表(Article)表服务实现类
 *
 * @author 晓风残月Lx
 * @since 2023-04-09 11:01:24
 */
@Service("articleService")
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

    @Resource
    private ArticleMapper articleMapper;

    @Resource
    private TagsMapper tagsMapper;

    @Resource
    private SystemConfigService systemConfigService;

    @Resource
    private SearchStrategyContext searchStrategyContext;

    @Resource
    private CategoryMapper categoryMapper;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private ElasticSearchService elasticSearchService;


    /**
     * 后台查询文章
     *
     * @param map
     * @return
     */
    @Override
    public R listArticle(Map<String, Object> map) {
        Page<ArticleListVO> articleListVOPage = baseMapper.selectListRecord(new Page<>((Integer) map.get("pageNo"), (Integer) map.get("pageSize")), map);
        return R.success(articleListVOPage);
    }

    /**
     * 置顶文章或取消置顶
     *
     * @param articleDTO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R putTopArticle(ArticleTopDTO articleDTO) {
        baseMapper.putTopArticle(articleDTO);
        return R.success();
    }

    /**
     * 上架或是下架
     *
     * @param articleDTO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R pubOrShelf(ArticleTopDTO articleDTO) {
        baseMapper.pubOrShelf(articleDTO);

        Article article = baseMapper.selectById(articleDTO.getId());
        // 如果修改为下架，先删除
        if (article.getIsPublish().equals(Constant.ZERO)) {
            elasticSearchService.deleteBatch(Collections.singletonList(article.getId()));
        } else if (article.getIsPublish().equals(Constant.ONE)){
            // 如果修改为上架，重新新增
            elasticSearchService.addArticleSearchVO(article);
        }
        return R.success();
    }

    /**
     * 删除
     *
     * @param id
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R delete(Long id) {

        Article article = baseMapper.selectById(id);

        int deleteById = baseMapper.deleteById(id);
        if (deleteById > Constant.ZERO && article.getIsPublish().equals(PublishEnum.SHELF.getCode())) {
            deleteArticleTag(Collections.singletonList(id));
            elasticSearchService.deleteBatch(Collections.singletonList(id));
            return R.success();
        }
        return R.error("删除失败");
    }

    /**
     * 批量删除
     *
     * @param ids
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R deleteBatch(List<Long> ids) {

        int deleteBatchIds = baseMapper.deleteBatchIds(ids);
        if (deleteBatchIds > Constant.ZERO) {
            deleteArticleTag(ids);
            // 批量删除es
            elasticSearchService.deleteBatch(ids);
            return R.success();
        }
        return R.error("删除失败");
    }

    /**
     * 添加
     *
     * @param articleDTO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R add(ArticleDTO articleDTO) {
        if (articleDTO.getIsOriginal() == Constant.ZERO) {
            Assert.isTrue((articleDTO.getOriginalUrl() == null || "".equals(articleDTO.getOriginalUrl())), "请输入转载地址");
        }
        Article article = BeanCopyUtil.copyObject(articleDTO, Article.class);
        article.setUserId(StpUtil.getLoginIdAsLong());

        // 添加分类，将没有的分类进行新增
        Long categoryId = saveCategory(articleDTO.getCategoryName());
        // 添加标签，将没有的标签进行新增
        List<Long> tagsList = saveTagsList(articleDTO.getTags());

        article.setCategoryId(categoryId);
        int insert = baseMapper.insert(article);

        if (insert > Constant.ZERO) {
            tagsMapper.saveArticleTags(article.getId(), tagsList);
        }

        // 异步调用
        if(articleDTO.getIsPublish().equals(PublishEnum.PUBLISH.getCode())) {
            elasticSearchService.addArticleSearchVO(article);
        }

        return R.success();
    }

    /**
     * 获取文章详情
     *
     * @param id
     * @return
     */
    @Override
    public R info(Long id) {
        ArticleBackendInfoVO articleInfoVO = baseMapper.selectPrimaryKey(id);
        articleInfoVO.setTags(tagsMapper.selectByArticleId(id));
        return R.success(articleInfoVO);
    }

    /**
     * 更新文章
     *
     * @param articleDTO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R updateArticle(ArticleDTO articleDTO) {
        if (Objects.equals(articleDTO.getIsOriginal(), Constant.ZERO)) {
            Assert.isTrue((articleDTO.getOriginalUrl() != null || "".equals(articleDTO.getOriginalUrl())), "请输入转载地址");
        }
        // 查询图片是否改变
        Article plainArticle = baseMapper.selectById(articleDTO.getId());
        if (!plainArticle.getAvatar().equals(articleDTO.getAvatar())) {
            redisTemplate.opsForSet().remove(RedisConstant.DB_FILE_UPLOAD_IMAGES, plainArticle.getAvatar());
            redisTemplate.opsForSet().add(RedisConstant.DB_FILE_UPLOAD_IMAGES, articleDTO.getAvatar());
        }

        Article article = BeanCopyUtil.copyObject(articleDTO, Article.class);
        article.setUserId(StpUtil.getLoginIdAsLong());

        // 添加分类
        Long categoryId = saveCategory(articleDTO.getCategoryName());
        // 添加标签
        List<Long> tagList = saveTagsList(articleDTO.getTags());

        article.setCategoryId(categoryId);
        article.setUserId(StpUtil.getLoginIdAsLong());
        baseMapper.updateById(article);

        // 先删除所有标签映射关系
        tagsMapper.deleteArticleTag(Collections.singletonList(article.getId()));
        // 然后新增标签
        tagsMapper.saveArticleTags(article.getId(), tagList);

        // 异步调用
        if (article.getIsPublish().equals(PublishEnum.PUBLISH.getCode())) {
            elasticSearchService.updateArticleSearchVO(article);
        }

        return R.success();
    }

    /**
     * 爬取CSDN文章
     *
     * @param url
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R reptile(String url) {
        try {
            Document document = Jsoup.connect(url).get();
            // 文章的题目，"title-article"：题目的class属性
            Elements title = document.getElementsByClass("title-article");
            // 文章的标签，"tag-link"：标签的class属性
            Elements tags = document.getElementsByClass("tag-link");
            // 文章的内容，"article_content":文章内容的class属性
            Elements content = document.getElementsByClass("article_content");

            Elements select = document.select("meta[name=description]");

            if (StringUtils.isBlank(content.toString())) {
                throw new CustomerException(ResultCode.CRAWLING_ARTICLE_FAILED.getDesc());
            }

            // 爬取的是HTML内容，转成MD的内容
            String newContent = content.get(0).toString().replaceAll("<code>", "<code class=\"lang-java\">");
            MutableDataSet options = new MutableDataSet();
            String markdown = FlexmarkHtmlConverter.builder(options).build().convert(newContent)
                    .replace("lang-java", "java");

            //文章封面图片 由https://api.btstu.cn/该网站随机获取
            String strResult = restTemplate.getForObject(Constant.IMG_URL_API, String.class);
            JSONObject jsonObject = JSON.parseObject(strResult);
            Object imgUrl = jsonObject.get("imgurl");

            Category category = categoryMapper.selectOne(new QueryWrapper<Category>().eq(SqlConstant.NAME, Constant.OTHER_CATEGORY_ID));

            Article article = Article.builder().userId(StpUtil.getLoginIdAsLong()).contentMd(markdown).
                    categoryId(category.getId()).title(title.get(0).text()).content(newContent).avatar(imgUrl.toString())
                    .isOriginal(YesOrNoEnum.NO.getCode()).originalUrl(url).summary(select.get(0).attr("content")).build();

            baseMapper.insert(article);

            // 添加文章标签
            List<Long> tagsId = new ArrayList<>();

            tags.forEach(item -> {
                String tag = item.text();
                Tags result = tagsMapper.selectOne(new QueryWrapper<Tags>().eq(SqlConstant.NAME, tag));
                if (result == null) {
                    result = Tags.builder().name(tag).sort(Constant.ZERO).build();
                    tagsMapper.insert(result);
                }
                tagsId.add(result.getId());
            });
            tagsMapper.saveArticleTags(article.getId(), tagsId);

            // 异步调用
            if(article.getIsPublish().equals(PublishEnum.PUBLISH.getCode())) {
                elasticSearchService.addArticleSearchVO(article);
            }
        } catch (IOException e) {
            throw new CustomerException(e);
        }
        return R.success();
    }


    // Web 端开始

    /**
     * 获取文章列表
     *
     * @param pageNo
     * @param pageSize
     * @return
     */
    @Override
    public R listWebArticle(Integer pageNo, Integer pageSize) {
        Page<ArticlePreviewVO> articlePreviewVOPage = articleMapper.selectPreviewPage(new Page<>(pageNo, pageSize), PublishEnum.PUBLISH.getCode(), null, null);
        articlePreviewVOPage.getRecords().forEach(item -> item.setTagVOList(tagsMapper.findByArticleIdToTags(item.getId())));
        return R.success(articlePreviewVOPage);
    }

    /**
     * 搜索文章
     *
     * @param keywords 搜索关键词
     * @return
     */
    @Override
    public R searchArticle(String keywords) {
        if (StringUtils.isEmpty(keywords)) {
            throw new RuntimeException();
        }

        // 获取搜索模式
        SystemConfig systemConfig = systemConfigService.getCustomizeOne();
        String strategy = SearchEnum.getStrategy(systemConfig.getSearchModel());

        // 搜索逻辑
        List<ArticleSearchVO> articleSearchVOS = searchStrategyContext.executeSearchStrategy(strategy, keywords);
        return R.success(articleSearchVOS);
    }

    /**
     * 获取文章详情
     *
     * @param id
     * @return
     */
    @Override
    public R webArticleInfo(Long id) {
        ArticleInfoVO articleInfoVO = baseMapper.selectPrimaryKeyById(id);

        // 标签
        List<TagVO> tags = tagsMapper.findByArticleIdToTags(articleInfoVO.getId());
        articleInfoVO.setTags(tags);

        // 分类
        Category category = categoryMapper.selectOne(new LambdaQueryWrapper<Category>()
                .select(Category::getId, Category::getName)
                .eq(Category::getId, articleInfoVO.getCategoryId()));

        articleInfoVO.setCategoryId(category.getId());
        articleInfoVO.setCategoryName(category.getName());

        // 查询上下篇
        ArticleVO lastArticle = baseMapper.getNextOrLastArticle(id, 0, PublishEnum.PUBLISH.getCode());
        ArticleVO nextArticle = baseMapper.getNextOrLastArticle(id, 1, PublishEnum.PUBLISH.getCode());
        articleInfoVO.setLastArticle(lastArticle);
        articleInfoVO.setNextArticle(nextArticle);

        //最新文章
        List<ArticleVO> blogArticles = baseMapper.getNewArticles(id, PublishEnum.PUBLISH.getCode());
        articleInfoVO.setNewestArticleList(blogArticles);

        // 推荐
        List<ArticleVO> recommendArticleList = baseMapper.listRecommendArticles(id);
        articleInfoVO.setRecommendArticleList(recommendArticleList);

        // 获取点赞量
        Long size = redisTemplate.opsForSet().size(RedisConstant.ARTICLE_LIKE_COUNT + id);
        articleInfoVO.setLikeCount(size);

        if (StpUtil.isLogin()) {// 记录浏览过该文章
            redisTemplate.opsForList().leftPush(RedisConstant.ARTICLE_USER_HISTORY + StpUtil.getLoginIdAsInt(), id);
        }

        // 文章浏览量 + 1
        redisTemplate.opsForHash().increment(RedisConstant.ARTICLE_VIEWS_COUNT, id.toString(), Constant.LONG_ONE);

        // 获取阅读量
        Object o = redisTemplate.opsForHash().get(RedisConstant.ARTICLE_VIEWS_COUNT, id.toString());
        if (o != null) {
            Long quantity = JSONObject.parseObject(o.toString(), Long.class);
            articleInfoVO.setQuantity(quantity);
        }
        return R.success(articleInfoVO);
    }


    /**
     * 获取分类or标签文章
     *
     * @param categoryId
     * @param tagsId
     * @param pageNo
     * @param pageSize
     * @return
     */
    @Override
    public R condition(Long categoryId, Long tagsId, Integer pageNo, Integer pageSize) {
        HashMap<String, Object> result = new HashMap<>();

        // 查询同分类的文章(按点赞量最高和浏览量最高排序)
        Page<ArticleCategoryVO> articlePage = articleMapper.selectPageByCategoryId(new Page<>(pageNo, pageSize), PublishEnum.PUBLISH.getCode(), categoryId, tagsId);

        articlePage.getRecords().forEach(item -> {
            List<TagVO> byArticleIdToTags = tagsMapper.findByArticleIdToTags(item.getId());
            item.setTagVOList(byArticleIdToTags);
        });

        String name;

        if (categoryId != null) {
            name = categoryMapper.selectById(categoryId).getName();
            incrCategory(categoryId);
        } else {
            name = tagsMapper.selectById(tagsId).getName();
            incrTags(tagsId);
        }
        result.put(SqlConstant.NAME, name);
        result.put(Constant.RECORDS, articlePage.getRecords());

        return R.success(result);
    }

    /**
     * 记录分类点击
     *
     * @param categoryId
     */

    private void incrCategory(Long categoryId) {
        Map<String, Object> map = redisTemplate.opsForHash().entries(RedisConstant.CATEGORY_CLICK_VOLUME);
        Integer value = (Integer) map.get(categoryId.toString());
        // 如果key存在就直接加一
        if (value != null) {
            map.put(categoryId.toString(), value + Constant.ONE);
        } else {
            map.put(categoryId.toString(), Constant.ONE);
        }
        redisTemplate.opsForHash().putAll(RedisConstant.CATEGORY_CLICK_VOLUME, map);
    }

    /**
     * 记录标签点击
     *
     * @param tagsId
     */
    private void incrTags(Long tagsId) {
        Map<String, Object> map = redisTemplate.opsForHash().entries(RedisConstant.TAG_CLICK_VOLUME);
        Integer value = (Integer) map.get(tagsId.toString());
        // 如果key存在就直接加一
        if (value != null) {
            map.put(tagsId.toString(), value + Constant.ONE);
        } else {
            map.put(tagsId.toString(), Constant.ONE);
        }
        redisTemplate.opsForHash().putAll(RedisConstant.TAG_CLICK_VOLUME, map);
    }

    /**
     * 归档
     *
     * @param pageNo
     * @param pageSize
     * @return
     */
    @Override
    public R archive(Integer pageNo, Integer pageSize) {
        Page<ArticleArchiveVO> voPage = articleMapper.selectArchivePage(new Page<>(pageNo, pageSize), PublishEnum.PUBLISH.getCode());

        return R.success(voPage);
    }

    /**
     * 点赞或取消点赞
     *
     * @param articleId
     * @return
     */
    @Override
    public R articleLike(Integer articleId) {
        Long loginId = StpUtil.getLoginIdAsLong();
        // 用户点赞的key
        String userLike = RedisConstant.ARTICLE_USER_LIKE + loginId;

        // 文章点赞量的key
        String articleLike = RedisConstant.ARTICLE_LIKE_COUNT + articleId.toString();

        String script = "if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then " +
                "redis.call('SREM', KEYS[1], ARGV[1]) " +
                "redis.call('SREM', KEYS[2], ARGV[2]) " +
                "return 0 " +
                "elseif redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 0 then " +
                "redis.call('SADD', KEYS[1], ARGV[1]) " +
                "redis.call('SADD', KEYS[2], ARGV[2]) " +
                "return 1 " +
                "else " +
                "return -1 " +
                "end";

        Long execute = (Long) redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(articleLike, userLike), loginId, articleId);

        // 在Long中有一个缓存内部类，在-128到127不会创建新对象，而是直接返回同一对象
        if (execute == Constant.LONG_ZERO) {
            return R.success("取消点赞成功");
        } else if (execute == Constant.LONG_ONE) {
            return R.success("点赞成功");
        } else {
            return R.error();
        }
    }


    //    -----自定义方法开始-------

    /**
     * 删除文章后的一些同步删除
     *
     * @param ids
     */
    private void deleteArticleTag(List<Long> ids) {
        tagsMapper.deleteArticleTag(ids);
        //异步删除es文章
    }

    /**
     * 如果分类不存在就新增
     *
     * @param categoryName
     * @return
     */
    private Long saveCategory(String categoryName) {
        Category category = categoryMapper.selectOne(new QueryWrapper<Category>().eq(SqlConstant.NAME, categoryName));
        if (category == null) {
            category.setName(categoryName);
            category.setSort(Constant.ZERO);
            categoryMapper.insert(category);
        }
        return category.getId();
    }

    /**
     * 如果标签不存在就新增
     *
     * @param tags
     * @return
     */
    private List<Long> saveTagsList(List<String> tags) {
        List<Long> tagList = new ArrayList<>();

        tags.forEach(item -> {
            Tags tag = tagsMapper.selectOne(new QueryWrapper<Tags>().eq(SqlConstant.NAME, item));
            if (tag == null) {
                tag = Tags.builder().name(item).sort(Constant.ZERO).build();
                tagsMapper.insert(tag);
            }
            tagList.add(tag.getId());
        });
        return tagList;
    }


}

